package com.baidu.ueditor.upload;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.codec.binary.Base64;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baidu.ueditor.PathFormat;
import com.baidu.ueditor.define.AppInfo;
import com.baidu.ueditor.define.BaseState;
import com.baidu.ueditor.define.FileType;
import com.baidu.ueditor.define.State;

/**
 * 以BASE64文本上传文件内容
 *
 * @author ueditor
 * @author zhaohuihua 20200527 优化处理逻辑
 */
public final class Base64Uploader {

    private IStorageManager storage;

    public Base64Uploader(IStorageManager storage) {
        this.storage = storage;
    }

    // data:text/plain,<文本数据>
    // data:text/html,<HTML代码>
    // data:text/html;base64,<base64编码的HTML代码>
    // data:text/css,<CSS代码>
    // data:text/css;base64,<base64编码的CSS代码>
    // data:text/javascript,<Javascript代码>
    // data:text/javascript;base64,<base64编码的Javascript代码>
    // 编码的gif图片数据
    // 编码的png图片数据
    // 编码的jpeg图片数据
    // 编码的icon图片数据
    private Pattern DATA_PREFIX = Pattern.compile("^data:([-\\w]+)/([-\\w]+)(;base64)?,", Pattern.CASE_INSENSITIVE);

    public State save(HttpServletRequest request, Map<String, Object> conf) {
        String filedName = (String) conf.get("fieldName");
        Map<String, String> parameters;
        try {
            parameters = getRequestParameter(request, filedName);
        } catch (IOException e) {
            return new BaseState(false, AppInfo.IO_ERROR);
        }

        String originFileName = parameters.get("fileName");
        String content = parameters.get(filedName);
        if (content == null || content.length() == 0) {
            return new BaseState(false, AppInfo.IO_ERROR);
        }
        Matcher matcher = DATA_PREFIX.matcher(content);
        byte[] data;
        String suffix = null;
        if (!matcher.find()) {
            try {
                data = decode(content);
            } catch (Exception e) {
                return new BaseState(false, AppInfo.IO_ERROR);
            }
            if (originFileName != null) {
                suffix = FileType.getExtension(originFileName);
            }
        } else {
            // 
            content = content.substring(matcher.end());
            String dataType = matcher.group(1);
            String formatType = matcher.group(2);
            String base64 = matcher.group(3);
            suffix = FileType.getSuffix(formatType);
            if (base64 != null) {
                try {
                    data = decode(content);
                } catch (Exception e) {
                    return new BaseState(false, AppInfo.IO_ERROR);
                }
            } else {
                data = content.getBytes(Charset.forName("UTF-8"));
            }
            if (originFileName == null) {
                originFileName = dataType + suffix;
            }
        }

        long maxSize = ((Long) conf.get("maxSize")).longValue();

        if (!validSize(data, maxSize)) {
            return new BaseState(false, AppInfo.MAX_SIZE);
        }

        if (suffix != null && !validType(suffix, (String[]) conf.get("allowFiles"))) {
            return new BaseState(false, AppInfo.NOT_ALLOW_FILE_TYPE);
        }
        
        String filename;
        if (originFileName == null) {
            filename = generateUuid();
        } else {
            filename = FileType.removeExtension(new File(originFileName).getName());
        }

        String savePath = PathFormat.parse((String) conf.get("savePath"), filename);
        if (suffix != null) {
            savePath = savePath + suffix;
        }
        String rootPath = (String) conf.get("rootPath");

        State storageState = storage.saveBinaryFile(data, rootPath, savePath);

        if (storageState.isSuccess()) {
            storageState.putInfo("type", suffix);
            storageState.putInfo("original", originFileName);
        }

        return storageState;
    }

    private static byte[] decode(String content) {
        return Base64.decodeBase64(content);
    }

    private static boolean validSize(byte[] data, long length) {
        return data.length <= length;
    }

    private static boolean validType(String type, String[] allowTypes) {
        List<String> list = Arrays.asList(allowTypes);
        return list.contains(type);
    }

    /**
     * 生成随机序列号
     *
     * @return 序列号
     */
    private static String generateUuid() {
        return UUID.randomUUID().toString().replace("-", "");
    }

    private Map<String, String> getRequestParameter(HttpServletRequest request, String filedName) throws IOException {
        String content = request.getParameter(filedName);
        if (content != null) { // contentType=application/x-www-form-urlencoded
            Map<String, String> result = new HashMap<>();
            Map<String, String[]> params = request.getParameterMap();
            for (Map.Entry<String, String[]> entry : params.entrySet()) {
                result.put(entry.getKey(), joinToString(entry.getValue(), ','));
            }
            return result;
        } else { // contentType=application/json
            StringBuilder buffer = new StringBuilder();
            String line = null;
            BufferedReader reader = request.getReader();
            while ((line = reader.readLine()) != null) {
                buffer.append(line);
            }
            String string = buffer.toString().trim();
            if (string.startsWith("{") && string.endsWith("}")) {
                // json格式
                return parseJsonParams(string);
            } else {
                // parameter格式
                return parseRequestParams(string, "UTF-8");
            }
        }
    }
    
    /**
     * 将数组合并为字符串
     *
     * @param array 数组
     * @param seprator 分隔符, 可为空
     * @return 合并后的字符串
     */
    private static String joinToString(Object[] array, char seprator) {
        if (array == null) {
            return null;
        }
        StringBuilder buffer = new StringBuilder();
        if (array.length > 0) {
            for (Object tmp : array) {
                if (buffer.length() > 0) {
                    buffer.append(seprator);
                }
                buffer.append(tmp);
            }
        }
        return buffer.toString();
    }

    /**
     * 解析Json参数
     * 
     * @param params 请求参数
     * @return Map
     */
    private static Map<String, String> parseJsonParams(String params) {
        JSONObject json = JSON.parseObject(params);
        Map<String, String> result = new HashMap<>();
        for (String key : json.keySet()) {
            result.put(key, json.getString(key));
        }
        return result;
    }

    /**
     * 解析请求参数
     * 
     * @param params 请求参数
     * @param charset 字符编码: 如果不为空, 将对value执行URLDecoder.decode(value, charset)
     * @return Map
     */
    private static Map<String, String> parseRequestParams(String params, String charset) {
        Map<String, String> map = new HashMap<>();
        String[] array = split(params, '&');
        for (String item : array) {
            // 不能用StringTools.split('=');
            // 因为有可能存在多个等号: contentType=application/json;charset=UTF-8
            int index = item.indexOf('=');
            if (index < 0) {
                map.put(item, null);
            } else if (index == 0) {
                map.put(item, "");
            } else {
                String key = item.substring(0, index);
                String value = item.substring(index + 1);
                if (charset != null) {
                    try {
                        value = URLDecoder.decode(value, charset);
                    } catch (UnsupportedEncodingException ignore) {
                    }
                }
                map.put(key, value);
            }
        }
        return map;
    }

    /**
     * 按指定字符拆分字符串<br>
     * 每一个子字符串都已经trim()过了<br>
     * "aa|bb|cc" --&gt; [aa, bb, cc]<br>
     * "aa|bb||cc" --&gt; [aa, bb, , cc]
     *
     * @param string 原字符串
     * @param chars 分隔符
     * @return 拆分后的字符串数组
     */
    private static String[] split(String string, char... chars) {
        return split(string, true, chars);
    }

    /**
     * 按指定字符拆分字符串<br>
     * "aa|bb|cc" --&gt; [aa, bb, cc]<br>
     * "aa|bb||cc" --&gt; [aa, bb, , cc]
     *
     * @param string 原字符串
     * @param trim 每一个子字符串是否执行trim()
     * @param chars 分隔符
     * @return 拆分后的字符串数组
     */
    private static String[] split(String string, boolean trim, char... chars) {
        if (string == null) {
            return null;
        }
        if (string.length() == 0) {
            return new String[0];
        }
        if (chars == null || chars.length == 0) {
            return new String[] { string };
        }
        List<String> list = new ArrayList<>();
        StringBuilder buffer = new StringBuilder();
        char[] textChars = string.toCharArray();
        boolean lastIsSplitChar = false;
        for (int i = 0; i < textChars.length; i++) {
            char c = textChars[i];
            boolean isSplitChar = false;
            for (int j = 0; j < chars.length; j++) {
                if (c == chars[j]) {
                    isSplitChar = true;
                    break;
                }
            }
            if (!isSplitChar) {
                buffer.append(c);
                lastIsSplitChar = false;
            } else {
                if (trim) {
                    list.add(buffer.toString().trim());
                } else {
                    list.add(buffer.toString());
                }
                buffer.setLength(0);
                lastIsSplitChar = true;
            }
        }
        if (buffer.length() > 0) {
            if (trim) {
                list.add(buffer.toString().trim());
            } else {
                list.add(buffer.toString());
            }
            buffer.setLength(0);
        }
        if (lastIsSplitChar) {
            list.add("");
        }
        return list.toArray(new String[0]);
    }
}